#include <GL/glut.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <GL/glext.h>
#include <math.h>

#include "lc_array.h"
#include "pac_level.h"
#include "pac_object.h"
#include "pac_game.h"
#include "pac_logger.h"
#include "pac_values.h"
#include "pac_drawer3D.h"
#include "pac_drawer2D.h"
#include "pac_IHM.h"
#include "pac_sound_player.h"
#include "pac_tools.h"


int mousePosX = 0;
int mousePosY = 0;

boolean dragEnabled = FALSE;

boolean isClicking = FALSE;

float ex = 0;
float ey = 5;
float ez = 0;

// Valeur de l'angle de décalage
int userCamAngle = 0;

// Temps restant pour l'angle de décalage
int camAngleTime = 0;

// Booléen indiquant si la Map2D doit être affichée
boolean shouldDrawMap2D = FALSE;

// Vue courante affichée
int currentView;

// Taille courante de la fenetre
int windowSizeX, windowSizeY;

// Jeu de PacMan
PacGame * pacGame;


void displayText(void *font, char *message) {
    int i;
    for (i = 0; i < strlen(message); ++i){
        glutBitmapCharacter(font, message[i]);
    }
}

void printText(float posX, float posY, char * aString) {
    glPushMatrix();
    glRasterPos2f(posX, posY);
    displayText(GLUT_BITMAP_TIMES_ROMAN_24, aString);
    glPopMatrix();
}

void drawScore(PacGame * aPacGame) {

    char score[15];
    sprintf(score, "Score : %d", aPacGame->playerScore);
    glColor3f(0.9, 0.9, 0.9);
    printText(0.05,0.95,score);
}

void drawPauseInformation(PacGame * aPacGame) {
    pac_log(LOG_DEBUG, "drawPauseInformation\n");

    // Si le temps de pause restant est supérieur à zéro
    if (aPacGame->gamePauseTime > 0) {
        // Affichage du temps restant en rouge
        glColor3f(1, 1, 1);
        char pauseTime[2];
        sprintf(pauseTime, "%d", (int) ((aPacGame->gamePauseTime) + 700) / 1000) ;
        printText(0.5, 0.5, pauseTime);
    }
}

void drawLifeCount(PacGame * aPacGame) {
    char score[15];
    sprintf(score, "Vie : %d", aPacGame->playerLifeCount);
    glColor3f(0.9, 0.9, 0.9);
    printText(0.05,0.90,score);
}


void drawHUD(PacGame * aPacGame) {
    pac_log(LOG_DEBUG, "drawHUD\n");
   
    glMatrixMode(GL_PROJECTION);
    glPushMatrix();
    glLoadIdentity();
    gluOrtho2D(0, 1, 0, 1);
    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();
    glLoadIdentity();
    
    // Si la map 2D Doit être affichée
    if (shouldDrawMap2D) {
        // Affiche de la map 2D
        draw2DMap(aPacGame->currentLevel);
    }   

    // Affichage du score
    drawScore(aPacGame);
    
    // Affichage de l'eventuelle temps de pause
    drawPauseInformation(aPacGame);
    
    // Affichage du nombre de vie
    drawLifeCount(aPacGame);

   
    glPopMatrix();
    glMatrixMode(GL_PROJECTION);
    glPopMatrix();
    glMatrixMode(GL_MODELVIEW);

}



void displayGameOver(PacGame * aPacGame) {
            pac_log(LOG_DEBUG, "displayGameOver\n");

    // Affichage du score en rouge
    glColor3f(0.8, 0.1, 0.1);
    char score[20];
    sprintf(score, ">  %d  <", aPacGame->playerScore);
    printText(0.4, 0.1, score);

    // Affichage de l'image de GameOver
    drawFullScreenTex(GAMEOVER_MENU_TEX);
}

void displayGameWin(PacGame * aPacGame) {
            pac_log(LOG_DEBUG, "displayGameWin\n");
            
    // Affichage du score en rouge
    glColor3f(0.8, 0.1, 0.1);
    char score[20];
    sprintf(score, ">>  %d  <<", aPacGame->playerScore);
    printText(0.4, 0.15, score);

    // Affichage de l'image de Victoire
    drawFullScreenTex(WIN_MENU_TEX);
}

void displayMainMenu(PacGame * aPacGame){
            pac_log(LOG_DEBUG, "displayMainMenu\n");
    drawFullScreenTex(MAIN_MENU_TEX);

}

void displayPauseMenu(PacGame * aPacGame){
            pac_log(LOG_DEBUG, "displayPauseMenu\n");
    drawFullScreenTex(PAUSE_MENU_TEX);
}


void displayMenu(PacGame * aPacGame){
        pac_log(LOG_DEBUG, "displayMenu\n");
        
    glMatrixMode(GL_PROJECTION);
    glPushMatrix();
    glLoadIdentity();
    gluOrtho2D(0, 1, 0, 1);
    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();
    glLoadIdentity();

    // Affichage de la page de menu
    switch (currentView) {

            // Page de GameOver
        case GAME_OVER_VIEW:
            displayGameOver(aPacGame);
            break;

            // Page de Victoire
        case GAME_WIN_VIEW:
            displayGameWin(aPacGame);
            break;

            // Page de Pause
        case PAUSE_VIEW:
            displayPauseMenu(aPacGame);
            break;

            // Page de menu principal
        default:
            displayMainMenu(aPacGame);
            break;
    }
    
    glPopMatrix();
    glMatrixMode(GL_PROJECTION);
    glPopMatrix();
    glMatrixMode(GL_MODELVIEW);
}

float calculateCameraTargetX(float originalPosX, float originalPosZ, float rotateAngle){
                      
    // Calcul de la position trigonométrique de X' en fonction de la rotation et de X et Y
    return (originalPosX * cos(rotateAngle)) - (originalPosZ * sin(rotateAngle));
}

float calculateCameraTargetZ(float originalPosX, float originalPosZ, float rotateAngle){

        // Calcul de la position trigonométrique de Y' en fonction de la rotation et de X et Y
    return (originalPosX * sin(rotateAngle)) + (originalPosZ * cos(rotateAngle));
}



void updateCameraPosition(PacGame * aPacGame){

    PacObject * pacMan = aPacGame->currentLevel->pacMan;
    float pacPosX = pacMan->posX;
    float pacPosY = pacMan->posY;
    
    // Si l'on est en mode FPS
    if (aPacGame->isFPSMode) {

        // La caméra est placé légèrement au dessus de pacman (pour voir la map)
        float camPosX = -pacPosX +0.5 ;
        float camPosY = 1.5;
        float camPosZ = pacPosY + 0.5;

        // La position en Y de la cible est située en dessous de la map.
        float targetPosY = -5;
        
        // Les positions en X et Z de la cible sont initialisée vers le haut.
        float toUpPosX = camPosX;
        float toUpPosZ = camPosZ + 15;
        
        int angle = 0;
        
        switch (pacMan->currentMove){
            case MOVE_DOWN :
                angle = 180;
                break;
            case MOVE_RIGHT:
                angle = 90;
                break;
            case MOVE_LEFT:
                angle = -90;
        }
        
        // Calcul des valeur réelles de X et Z de la cible en fonction de  l'angle de la caméra
        double radianAngle  =  M_PI * (aPacGame->cameraAngle + userCamAngle) / 180.0 ;
        
        double x_x0 = toUpPosX - camPosX;
        double y_y0 = toUpPosZ - camPosZ;
        
        double targetPosX = calculateCameraTargetX(x_x0, y_y0, radianAngle) + camPosX;
        double targetPosZ = calculateCameraTargetZ(x_x0, y_y0, radianAngle)  + camPosZ;
        
        gluLookAt(camPosX, camPosY, camPosZ, targetPosX, targetPosY, targetPosZ, 0, 1.0, 0.0);
    } else {
        
        // La caméra est située légèrement en dessous de PacMan
        // Elle est ajustée en fonction des déplacements de l'utilisateur
        float camPosX = -pacPosX + 0.5 + ex;
        float camPosY = ey + 5 ;
        float camPosZ = pacPosY + ez -5;

        // La cible est situé en haut de PacMan et légèrement sous la map.
        float targetPosX = camPosX;
        float targetPosY = -5;
        float targetPosZ = camPosZ + 10;
        
        gluLookAt(camPosX, camPosY, camPosZ, targetPosX, targetPosY, targetPosZ, 0, 1.0, 0.0);
    }
}


void displayGame(PacGame * aPacGame){
    pac_log(LOG_DEBUG, "displayGame\n");
        glLoadIdentity();
    
    // Mise à jour de la position de la caméra
    updateCameraPosition(aPacGame);
    
        // Dessin du HUD
    drawHUD(aPacGame);
   
    // Dessin de la map 3D
    draw3DMap(aPacGame);
     


}

void updateCurrentView(PacGame * aPacGame) {
    // Si la partie est perdu
    if (aPacGame->gameIsOver) {

        // Affichage de la page de GameOver
        currentView = GAME_OVER_VIEW;
    } else
        // Si la partie est gagnée
        if (aPacGame->gameIsWon) {

        // Affichage de la page de Victoire
        currentView = GAME_WIN_VIEW;
    }
    else
        // Si la partie est en pause non temporaire
        if (aPacGame->gameIsPaused && !aPacGame->gameWillAutoUnpause) {

        // Affichage du menu de pause
        currentView = PAUSE_VIEW;
    } else
        // Si la partie n'est pas commencée
        if (!aPacGame->gameIsStarted) {
        // Affichage du menu principal
        currentView = MAIN_MENU_VIEW;
    }
    else
        // Dans les autres cas
    {
        // Affichage de la vue du jeu
        currentView = GAME_VIEW;
    }
}

void displayCurrentView(PacGame * aPacGame) {
    pac_log(LOG_DEBUG, "displayCurrentView\n");

    // Si le jeu doit être affiché
    if (currentView == GAME_VIEW) {

        displayPauseMenu( aPacGame);
        // Affichage du jeu
        displayGame(aPacGame);
    } else {

        // Sinon, affichage du menu courant
        displayMenu(aPacGame);
    }
}

void display(void) {
    pac_log(LOG_DEBUG, "display\n");

    // Remise à zéro des buffers
    glLoadIdentity();
    glClear(GL_COLOR_BUFFER_BIT);
    glClear(GL_DEPTH_BUFFER_BIT);
    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);

    // Mise à jour éventuelle de la vue courante
    updateCurrentView(pacGame);
    
    // Affichage de la vue courante
    displayCurrentView(pacGame);

    glutSwapBuffers();
}

void reshape(int w, int h) {
    windowSizeX = w;
    windowSizeY = h;
    
    pac_log(LOG_DEBUG, "reshape\n");
    glViewport(0, 0, (GLsizei) w, (GLsizei) h);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(60.0, 1, 0.5, 1000.0);
    glMatrixMode(GL_MODELVIEW);
}

void returnToMainMenu(PacGame * aPacGame){
    currentView = MAIN_MENU_VIEW;
    aPacGame->gameIsStarted = FALSE;
    aPacGame->gameIsOver = FALSE;
    aPacGame->gameIsPaused = FALSE;
    aPacGame->gameIsWon =FALSE;
}

void doRestartGame(PacGame * aPacGame){
        restartGame(pacGame);
}

void doGuitGame(){
    freePacSoundPlayer();
    freePacGame(pacGame);
    lcMemoryFree();
    exit(0);
}

void keyboard(unsigned char key, int x, int y) {
    pac_log(LOG_DEBUG, "keyboard\n");
    switch (key) {
 
        // -- Touches directionnelles
        
        case 'z':
            changePacmanDirection(pacGame, MOVE_UP);
            break;
        case 's':
            changePacmanDirection(pacGame, MOVE_DOWN);
            break;
        case 'q':
            changePacmanDirection(pacGame, MOVE_LEFT);
            break;
        case 'd':
            changePacmanDirection(pacGame, MOVE_RIGHT);
            break;

            // --- Touches d'option 'Pause' et 'Sons'

        case 'm':
            // Permutation de l'activation du son
            swapSoundActivity();
            break;

        case 'p':
            // Si le jeu n'est pas en pause
            if (!pacGame->gameIsPaused) {
                // Mise en pause temporaire.
                pauseGameDuring(pacGame, 4);
            }
            break;
            
            // --- Le pavé numérique modifie l'angle du mode FPS
            
       // NumPad_1
        case 49:
            userCamAngle = -135;
            camAngleTime = 500;
            break;
            
            // NumPad_2
        case 50:
            userCamAngle = 180;
            camAngleTime = 500;
            break;
        
            // NumPad_3
        case 51:
            userCamAngle = 135;
            camAngleTime = 500;
            break;
            
            // NumPad_4
        case 52:
            userCamAngle = -90;
            camAngleTime = 500;
            break;
            
            // NumPad_6
        case 54:
            userCamAngle = 90;
            camAngleTime = 500;
            break;
            
        // NumPad_7
        case 55:
            userCamAngle = -45;
            camAngleTime = 500;
            break;
            
       // NumPad_8
        case 56:
            userCamAngle = 0;
            break;
            
        // NumPad_9
        case 57:
            userCamAngle = 45;
            camAngleTime = 500;
            break;

            
            // Touche 'Echap'
        case 27:
            // Si le menu pause est affiché
            if (currentView == PAUSE_VIEW) {

                // Desactivation de la pause
                unpauseGame(pacGame);
            } else
                // Si le jeu est affiché
                if (currentView == GAME_VIEW) {
                // Mise en pause du jeu
                pauseGame(pacGame);
            } else
                // Si le c'est le menu principal
                if (currentView == MAIN_MENU_VIEW) {
                doGuitGame();
            } else {
                // Retour au menu principal
                returnToMainMenu(pacGame);
            }
            break;



            // Touche 'Tabulation'
        case 9:
            // Si la map2D est affichée
            if (shouldDrawMap2D) {
                // Elle ne le sera plus
                shouldDrawMap2D = FALSE;
            } else {
                // Sinon, elle le sera
                shouldDrawMap2D = TRUE;
            }
            break;


            // Touche 'Entrée'
        case 13:
            // Si c'est le menu principal
            if (currentView == MAIN_MENU_VIEW) {

                // Lancement de la partie
                doRestartGame(pacGame);
            }
            break;

            // Touche 'Espace'
        case 32:
            // Si le jeu est en mode FPS
            if (pacGame->isFPSMode) {
                // Passage en mode non-FPS
                pacGame->isFPSMode = FALSE;
                shouldDrawMap2D = FALSE;
            } else {
                //Sinon, Passage en mode FPS
                pacGame->isFPSMode = TRUE;
                shouldDrawMap2D = TRUE;                
            }
            
        default:
            // Do nothing...
            break;
    }
}

void executeClickOnPauseMenu(int posX, int posY) {

    // Calcul des positions relative de la souris
    int relativePosX = ((double) (posX / (double) windowSizeX)) * 100;
    int relativePosY = ((double) (posY / (double) windowSizeY)) * 100;

    // Si le curseur est sur 'Resume'
    if (relativePosX >= 14 && relativePosX <= 88 && relativePosY >= 34 && relativePosY <= 46) {

        // Désactivation de la pause
        unpauseGame(pacGame);
    } else
        // Si le curseur est sur 'Restart'
        if (relativePosX >= 6 && relativePosX <= 95 && relativePosY >= 55 && relativePosY <= 67) {
        // Rédémarrage de la partie
        doRestartGame(pacGame);

    } else
        // Si le curseur est sur 'Quit'
        if (relativePosX >= 26 && relativePosX <= 76 && relativePosY >= 74 && relativePosY <= 86) {
        // Retour au menu général
        returnToMainMenu(pacGame);
    }
}

void executeClickOnMainMenu(int posX, int posY) {

    // Calcul des positions relative de la souris
    int relativePosX = ((double) (posX / (double) windowSizeX)) * 100;
    int relativePosY = ((double) (posY / (double) windowSizeY)) * 100;

    // Si la position horizontale est dans l'interval des textes
    if (relativePosX >= 24 && relativePosX <= 78) {

        // Si la souris est sur le mot 'Play'
        if (relativePosY >= 40 && relativePosY <= 53) {
            doRestartGame(pacGame);

        }
            // Sinon, si la souris est sur le mot 'Quit'
        else if (relativePosY >= 65 && relativePosY <= 77) {
            doGuitGame();
        }
    }
}


void applyClickOnMenuEvent(int posX, int posY) {

    // Selection de l'action selon de la page de menu
    switch (currentView) {

            // Page de GameOver
        case GAME_OVER_VIEW:
            // Retour au menu général
            returnToMainMenu(pacGame);
            break;

            // Page de Victoire
        case GAME_WIN_VIEW:
            // Retour au menu général
            returnToMainMenu(pacGame);
            break;

            // Page de Pause
        case PAUSE_VIEW:
            executeClickOnPauseMenu(posX, posY);
            break;

            // Page de menu principal
        case MAIN_MENU_VIEW:
            executeClickOnMainMenu(posX, posY);
            break;

            // Vue du jeu
        default:
            // Do nothing...
            break;
    }
}

void OnMouseButton(int button, int state, int posX, int posY) {
    mousePosX = posX;
    mousePosY = posY;

    switch (button) {
            // Click GAUCHE
        case 0:
            // Si un menu est affiché
            if (currentView != GAME_VIEW) {

                // Si c'est le relachement de la souris
                if (isClicking) {
                    // Lancement de l'action de click sur le menu
                    applyClickOnMenuEvent(posX, posY);
                    isClicking = FALSE;
                } else {
                    // Sinon, prise en compte du click lors du prochain relachement
                    isClicking = TRUE;
                }
            }
            break;

            // Click MOLETTE
        case 1:
            if (dragEnabled) {
                dragEnabled = 0;
            } else {
                dragEnabled = 1;
            }
            break;

            // Click DROIT
        case 2:
            break;

            // Action molette BAS
        case 3:
            if (!pacGame->isFPSMode && ey >= 0.5) {
                ey = ey - 0.5;
            }
            break;

            // Action molette HAUT
        case 4:
            if (!pacGame->isFPSMode && ey <= 50) {
                ey = ey + 0.5;
            }
            break;
    }
}

void OnMouseMove(int posX, int posY) {

    // Si le jeu est affiché et n'est pas e mode FPS
    if (currentView == GAME_VIEW && !pacGame->isFPSMode) {

        // Récupération de la différence de position de la souris
        float diffX = (posX - mousePosX);
        float diffY = (posY - mousePosY);

        // Si le déplacement est activé (click molette)
        if (dragEnabled) {
            // Déplacement de la vue
            if (diffX != 0) {
                // Si la map est déplaçable
                if ((ex <= 25 && diffX > 0) || (ex >= -levelGetSizeX(pacGame->currentLevel) && diffX < 0)) {
                    ex = ex + (diffX / 10.0);
                }
            }
            if (diffY != 0) {
                // Si la map est déplaçable
                if ((ez <= levelGetSizeY(pacGame->currentLevel) && diffY > 0) || (ez >= -25 && diffY < 0)) {
                    ez = ez + (diffY / 10.0);
                }
            }
        }
    }
    
        // Enregistrement de la position de la souris
        mousePosX = posX;
        mousePosY = posY;
}

PacLevel* createLevel(char * aLevelFilePath) {
    pac_log(LOG_DEBUG, "createLevel\n");
    // Création du nouveau niveau
    PacLevel* newLevel = levelCreate();

    // Chargement du contenu de la map.
    if (loadPacMap(newLevel, aLevelFilePath) < 0) {
        // Retourne null en cas d'erreur de chargement
        return NULL;
    };

    return newLevel;
}

void updateUserCamAngle(){
    
    if (camAngleTime > 0){
        camAngleTime -= INTER_STEP_TIME;
    }
    
    if (camAngleTime <= 0 ){
        userCamAngle = 0;
    }
}

void loopFunction(int x) {
    pac_log(LOG_DEBUG, "loopFunction\n");
    // Relancement de la fonction automatiquement
    glutTimerFunc(INTER_STEP_TIME, loopFunction, 1);

    // Si le jeu a démarré
    if (pacGame->gameIsStarted) {
        tick(INTER_STEP_TIME);
        // Lancement d'un step dans le jeu
        processGameStep(pacGame);
    }
    
    updateUserCamAngle();
    
    // Réaffichage de la vue
    glutPostRedisplay();
}

void startGAME(int appArgc, char** appArgv, LCarray *levelPathList) {
    pac_log(LOG_DEBUG, "startGAME\n");
    

    
    currentView = MAIN_MENU_VIEW;

    // Création de la liste des niveaux
    LCarray * levelList = lcArrayCreate();

    int i;
    // Parcours de la liste des noms de niveaux
    for (i = 0; i < lcArrayLength(levelPathList); i++) {
        // Chargement d'un nouveau niveau
        char * levelPath = lcArrayGet(levelPathList, i);
        PacLevel* newLevel = createLevel(levelPath);
        if (newLevel != NULL) {
            pac_log(LOG_DEBUG, "Ajout du niveau\n");
            // Ajout du nouveau niveau s'il a été chargé
            lcArrayAdd(levelList, newLevel);
        }

    }

    // Si des niveaux ont été chargés
    if (lcArrayLength(levelList) != 0) {

        // Création du jeu
        pacGame = createGame(levelList);

        // Initialisation des paramètre openGL
        glutInit(&appArgc, appArgv);
        glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);

        // Initialisation de la fenêtre
        glutInitWindowSize(600, 600);
        glutInitWindowPosition(100, 100);
        glutCreateWindow(appArgv[0]);

        // Activation des paramètre openGL
        glEnable(GL_DEPTH_TEST);
        glEnable(GL_TEXTURE_2D);

        // Chargement des textures
        load3DTextures();
        load2DTextures();

        // Ajout des évènements souris et clavier
        glutMouseFunc(OnMouseButton);
        glutMotionFunc(OnMouseMove);
        glutKeyboardFunc(keyboard);

        // Ajout de la fonction répétéé en boucle
        glutTimerFunc(INTER_STEP_TIME, loopFunction, 1);
        
        // Fonction pour animer la bouche de pacman.
         glutTimerFunc(INTER_STEP_TIME, updatePacMouth, 1);

        // Ajout des fonction de dessin
        glutReshapeFunc(reshape);
        glutDisplayFunc(display);

         pac_log(LOG_INFO,"LaunchingMainLoop\n");
        
             initSoundPlayer();
             
        // Lancement
        glutMainLoop();
    }// Sinon, sortie du jeu
    else {
        printf("ERREUR > Aucun niveau n'a pu être chargé!\n");
        exit(0);
    }
}